﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="FrameAnalyzer.cs" company="ScRePt">
//   Copyright © ScRePt 2010 - 2012
// </copyright>
// <summary>
//   The FrameAnalyzer class provides functionality to decide
//   the bounding box of a given Bitmap object containing
//   letterboxed video material.
// </summary>
// --------------------------------------------------------------------------------------------------------------------

namespace MP3D.Zoom
{
    #region Using Directives

    using System.Drawing;

    #endregion

    /// <summary>
    /// The FrameAnalyzer class provides functionality to decide
    /// the bounding box of a given Bitmap object containing
    /// letterboxed video material.
    /// </summary>
    /// <remarks>
    /// "Borrowed" from ProcessPlugins.ViewModeSwitcher.FrameAnalyzer class of MediaPortal 1.2.1 source.
    /// I am not claiming ownership!
    /// </remarks>
    public class FrameAnalyzer
    {
        #region Constants and Fields

        /// <summary>
        /// The bottom scan end fraction.
        /// </summary>
        private readonly float bottomScanEndFraction;

        /// <summary>
        /// The bottom scan start fraction.
        /// </summary>
        private readonly float bottomScanStartFraction;

        /// <summary>
        /// The hist b.
        /// </summary>
        private readonly int[] histB = new int[256];

        /// <summary>
        /// The hist g.
        /// </summary>
        private readonly int[] histG = new int[256];

        /// <summary>
        /// The hist r.
        /// </summary>
        private readonly int[] histR = new int[256];

        /// <summary>
        /// The left scan end fraction.
        /// </summary>
        private readonly float leftScanEndFraction;

        /// <summary>
        /// The left scan start fraction.
        /// </summary>
        private readonly float leftScanStartFraction;

        /// <summary>
        /// The max brightness treshold.
        /// </summary>
        private readonly int maxBrightnessTreshold;

        /// <summary>
        /// The min brightness treshold.
        /// </summary>
        private readonly int minBrightnessTreshold;

        /// <summary>
        /// The right scan end fraction.
        /// </summary>
        private readonly float rightScanEndFraction;

        /// <summary>
        /// The right scan start fraction.
        /// </summary>
        private readonly float rightScanStartFraction;

        /// <summary>
        /// The top scan end fraction.
        /// </summary>
        private readonly float topScanEndFraction;

        /// <summary>
        /// The top scan start fraction.
        /// </summary>
        private readonly float topScanStartFraction;

        /// <summary>
        /// The bottom end.
        /// </summary>
        private int bottomEnd;

        /// <summary>
        /// The bottom start.
        /// </summary>
        private int bottomStart;

        /// <summary>
        /// The frame.
        /// </summary>
        private Bitmap frame;

        /// <summary>
        /// The left end.
        /// </summary>
        private int leftEnd;

        /// <summary>
        /// The left start.
        /// </summary>
        private int leftStart;

        /// <summary>
        /// The right end.
        /// </summary>
        private int rightEnd;

        /// <summary>
        /// The right start.
        /// </summary>
        private int rightStart;

        /// <summary>
        /// The top end.
        /// </summary>
        private int topEnd;

        /// <summary>
        /// The top start.
        /// </summary>
        private int topStart;

        #endregion

        #region Constructors and Destructors

        /// <summary>
        /// Initializes a new instance of the <see cref="FrameAnalyzer"/> class.
        /// </summary>
        public FrameAnalyzer()
        {
            this.leftStart = 0;

            this.maxBrightnessTreshold = 40;
            this.minBrightnessTreshold = 16;

            this.topScanStartFraction = 0.30f;
            this.topScanEndFraction = 0.70f;
            this.bottomScanEndFraction = 0.70f;
            this.bottomScanStartFraction = 0.30f;

            this.leftScanStartFraction = 0.30f;
            this.leftScanEndFraction = 0.70f;
            this.rightScanStartFraction = 0.30f;
            this.rightScanEndFraction = 0.70f;
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// (For debugging) Draws a line in the frame being analyzed
        /// </summary>
        /// <param name="line">
        /// </param>
        /// <param name="start">
        /// </param>
        /// <param name="end">
        /// </param>
        /// <param name="c">
        /// </param>
        /// <param name="horizontal">
        /// </param>
        public void DrawLine(int line, int start, int end, Color c, bool horizontal)
        {
            for (int p = start; p <= end; p++)
            {
                if (horizontal)
                {
                    // horizontal line scan
                    this.frame.SetPixel(p, line, c);
                }
                else
                {
                    // vertical line scan
                    this.frame.SetPixel(line, p, c);
                }
            }
        }

        /// <summary>
        /// Find top and bottom bounds of a given bitmap. Performs a top down and bottom down scan
        /// to find the first top/bottom line that has content. Whether or not a line is 'content'
        /// is decided by the IsContent() method.
        /// </summary>
        /// <param name="frameBitmap">
        /// The frame to bind the bounds of.
        /// </param>
        /// <param name="bounds">
        /// The bounds.
        /// </param>
        /// <returns>
        /// True if analysis succeeded(ie is trustworthy) and false otherwise
        /// </returns>
        public bool FindBounds(Bitmap frameBitmap, ref Rectangle bounds)
        {
            this.frame = frameBitmap;

            int topLine = 0;
            int bottomLine = this.frame.Height - 1;
            int leftLine = 0;
            int rightLine = this.frame.Width - 1;

            bool foundTop = false;
            bool foundBottom = false;
            bool foundLeft = false;
            bool foundRight = false;

            this.topStart = (int)(this.topScanStartFraction * this.frame.Width);
            this.topEnd = (int)(this.topScanEndFraction * this.frame.Width);
            if (this.topEnd >= this.frame.Width)
            {
                this.topEnd--;
            }

            this.bottomStart = (int)(this.bottomScanStartFraction * this.frame.Width);
            this.bottomEnd = (int)(this.bottomScanEndFraction * this.frame.Width);
            if (this.bottomEnd >= this.frame.Width)
            {
                this.bottomEnd--;
            }

            this.leftStart = (int)(this.leftScanStartFraction * this.frame.Height);
            this.leftEnd = (int)(this.leftScanEndFraction * this.frame.Height);
            if (this.leftEnd >= this.frame.Height)
            {
                this.leftEnd--;
            }

            this.rightStart = (int)(this.rightScanStartFraction * this.frame.Height);
            this.rightEnd = (int)(this.rightScanEndFraction * this.frame.Height);
            if (this.rightEnd >= this.frame.Height)
            {
                this.rightEnd--;
            }

            // Top black bar binary search scan
            int mid = 1;
            int low = 1;
            int high = (int)(this.frame.Height * 0.25f);

            while (low <= high)
            {
                this.ScanLine(mid, this.topStart, this.topEnd, true);
                if (this.IsContent(this.topStart, this.topEnd))
                {
                    high = mid - 1;
                    topLine = mid;
                    foundTop = true;
                }
                else
                {
                    low = mid + 1;
                }

                mid = (low + high) / 2;
            }

            if (topLine == 0)
            {
                topLine = 1;
            }

            // vertical scan of left half of screen      
            for (int line = 1; line < this.frame.Width * 0.25f; line++)
            {
                this.ScanLine(line, topLine, this.frame.Height - topLine, false);
                if (this.IsContent(this.leftStart, this.leftEnd))
                {
                    leftLine = line;
                    foundLeft = true;
                    break;
                }
            }

            if (!foundLeft && !foundTop)
            {
                bounds.Y = 0;
                bounds.X = 0;
                bounds.Height = this.frame.Height;
                bounds.Width = this.frame.Width;
                return true;
            }

            // vertical scan of right half of screen
            for (int line = this.frame.Width - 1; line > this.frame.Width * 0.75f; line--)
            {
                this.ScanLine(line, topLine, this.frame.Height - topLine, false);
                if (this.IsContent(this.rightStart, this.rightEnd))
                {
                    rightLine = line;
                    foundRight = true;
                    break;
                }
            }

            if (!foundLeft && !foundRight)
            {
                bounds.Y = 0;
                bounds.X = 0;
                bounds.Height = this.frame.Height;
                bounds.Width = this.frame.Width;
                return true;
            }

            // Bottom black bar binary search scan
            low = (int)(this.frame.Height * 0.75f);
            high = this.frame.Height - 1;
            mid = high;
            while (low <= high)
            {
                this.ScanLine(mid, this.bottomStart, this.bottomEnd, true);
                if (this.IsContent(this.bottomStart, this.bottomEnd))
                {
                    low = mid + 1;
                    bottomLine = mid;
                    foundBottom = true;
                }
                else
                {
                    high = mid - 1;
                }

                mid = (low + high) / 2;
            }

            if (!foundBottom || bottomLine - topLine < this.frame.Height * 0.25f || rightLine - leftLine < this.frame.Width * 0.25f)
            {
                return false;
            }

            bounds.Y = topLine;
            bounds.X = leftLine;
            bounds.Height = bottomLine - topLine;
            bounds.Width = rightLine - leftLine;
            return true;
        }

        #endregion

        #region Methods

        /// <summary>
        /// Determines if the last line scanned was content or not
        /// </summary>
        /// <param name="start">
        /// The start.
        /// </param>
        /// <param name="end">
        /// The end.
        /// </param>
        /// <returns>
        /// The is content.
        /// </returns>
        private bool IsContent(int start, int end)
        {
            int maxR = 0;
            int maxG = 0;
            int maxB = 0;
            int sumR = 0;
            int sumG = 0;
            int sumB = 0;

            // Check for brightest pixel value
            for (int i = 0; i < 255; i++)
            {
                if (this.histR[i] > 0 && i >= maxR)
                {
                    maxR = i;
                }

                if (this.histG[i] > 0 && i >= maxG)
                {
                    maxG = i;
                }

                if (this.histB[i] > 0 && i >= maxB)
                {
                    maxB = i;
                }

                if (i >= this.minBrightnessTreshold)
                {
                    sumR = sumR + this.histR[i];
                    sumG = sumG + this.histG[i];
                    sumB = sumB + this.histB[i];
                }
            }

            // At least one pixel with brightness level over 40 is found
            if (maxR > this.maxBrightnessTreshold || maxG > this.maxBrightnessTreshold || maxB > this.maxBrightnessTreshold)
            {
                return true;
            }

            // Over half of the number of pixels are above the brightness treshold
            if (sumR > ((end - start) / 2) || sumG > ((end - start) / 2) || sumB > ((end - start) / 2))
            {
                return true;
            }

            // No content detected
            return false;
        }

        /// <summary>
        /// Resets the RGB histograms
        /// </summary>
        private void ResetHistograms()
        {
            for (int i = 0; i < this.histR.Length; i++)
            {
                this.histR[i] = 0;
                this.histG[i] = 0;
                this.histB[i] = 0;
            }
        }

        /// <summary>
        /// Scans a line in the frame, producing R,G and B histograms
        /// </summary>
        /// <param name="line">
        /// The line to scan
        /// </param>
        /// <param name="start">
        /// How far into the line to start scan (to avoid logos etc)
        /// </param>
        /// <param name="end">
        /// How far into the line to stop the scan (to avoid logos etc) 
        /// </param>
        /// <param name="horizontal">
        /// Decides if this is a horizontal line scan (or vertical) 
        /// </param>
        private void ScanLine(int line, int start, int end, bool horizontal)
        {
            this.ResetHistograms();
            Color c;

            for (int p = start; p <= end; p++)
            {
                c = horizontal ? this.frame.GetPixel(p, line) : this.frame.GetPixel(line, p);

                this.histG[0xFF & c.G]++;
                this.histR[0xFF & c.R]++;
                this.histB[0xFF & c.B]++;
            }
        }

        #endregion
    }
}